package com.wxy.favorites.service;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import com.wxy.favorites.config.AppConfig;
import com.wxy.favorites.constant.ErrorConstants;
import com.wxy.favorites.constant.PublicConstants;
import com.wxy.favorites.core.ApiResponse;
import com.wxy.favorites.core.BizException;
import com.wxy.favorites.core.NoRollbackException;
import com.wxy.favorites.core.PageInfo;
import com.wxy.favorites.dao.UserFileRepository;
import com.wxy.favorites.dao.UserRepository;
import com.wxy.favorites.dto.DownloadFileDto;
import com.wxy.favorites.dto.FilePageDto;
import com.wxy.favorites.dto.ShareFileDto;
import com.wxy.favorites.dto.UserCapacityDto;
import com.wxy.favorites.entity.User;
import com.wxy.favorites.entity.UserFile;
import com.wxy.favorites.security.ContextUtils;
import com.wxy.favorites.security.SecurityUser;
import com.wxy.favorites.util.AssertUtils;
import com.wxy.favorites.util.SqlUtils;
import com.wxy.favorites.util.ZipUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * @Author wangxiaoyuan
 * @Date 2020/4/24 11:50
 * @Description
 **/
@Slf4j
@Service
@Transactional(noRollbackFor = NoRollbackException.class)
public class UserFileService {

    @Autowired
    private AppConfig appConfig;

    @Autowired
    private UserFileRepository userFileRepository;

    @Autowired
    private UserRepository userRepository;

    public UserFile save(UserFile userFile) {
        return userFileRepository.save(userFile);
    }

    public UserFile update(UserFile userFile) {
        return userFileRepository.save(userFile);
    }

    public UserFile findById(Integer id) {
        return userFileRepository.findById(id).orElse(null);
    }

    public List<UserFile> findByPid(Integer pid) {
        return userFileRepository.findByPid(pid);
    }

    public List<UserFile> findRootList(Integer userId) {
        return userFileRepository.findByUserIdAndPid(userId, 0);
    }

    /**
     * 将用户所有文件，按照层级结构，打包到临时目录
     *
     * @param userId
     * @param tempPath
     * @return
     * @throws IOException
     */
    public Path packageFile(Integer userId, String tempPath) throws IOException {
        // 查询用户文件
        List<UserFile> rootList = userFileRepository.findByUserIdAndPid(userId, 0);
        if (!CollectionUtils.isEmpty(rootList)) {
            Path root = Paths.get(tempPath, String.valueOf(userId));
            // 打包前，删除历史打包
            cleanHistory(root);
            createFile(rootList, root);
            return root;
        } else {
            return null;
        }
    }

    public void cleanHistory(Path base) throws IOException {
        if (Files.exists(base)) {
            Files.walkFileTree(base, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    Files.delete(file);
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                    Files.delete(dir);
                    return FileVisitResult.CONTINUE;
                }
            });
        }
    }

    private void createFile(List<UserFile> list, Path base) throws IOException {
        if (Files.notExists(base)) {
            Files.createDirectories(base);
        }
        for (UserFile file : list) {
            if (PublicConstants.DIR_CODE.equals(file.getIsDir())) {// 如果是文件夹，则创建文件夹
                Path director = Paths.get(base.toString(), file.getFilename());
                Files.createDirectories(director);
                // 查询文件夹下的文件并创建
                List<UserFile> children = userFileRepository.findByPid(file.getId());
                createFile(children, director);
            } else {// 如果是文件，则拷贝文件到临时目录
                Path source = Paths.get(file.getPath());
                if (Files.exists(source)) {
                    Path target = Paths.get(base.toString(), file.getFilename());
                    Files.copy(source, target);
                }
            }
        }
    }

    public void deleteById(Integer id, Integer userId) throws IOException {
        UserFile userFile = userFileRepository.findById(id).orElse(null);
        if (userFile != null) {
            List<UserFile> deletingFiles = new ArrayList<>();
            addDeletingFile(deletingFiles, userFile);
            long totalSize = 0L;
            List<String> pathList = new ArrayList<>();
            for (UserFile file : deletingFiles) {
                if (!PublicConstants.DIR_CODE.equals(file.getIsDir())) {
                    totalSize += file.getSize();
                    pathList.add(file.getPath());
                }
            }
            // 物理删除文件(目录不删)
            for (String path : pathList) {
                Files.deleteIfExists(Paths.get(path));
            }
            // 更新容量
            User user = userRepository.getOne(userId);
            long size = Optional.ofNullable(user.getUsedSize()).orElse(0L) - totalSize;
            user.setUsedSize(size < 0 ? 0 : size);// 容量计算误差修正
            userRepository.save(user);
            // 数据删除
            userFileRepository.deleteAll(deletingFiles);
        }
    }

    private void addDeletingFile(List<UserFile> deletingFiles, UserFile userFile) {
        if (PublicConstants.DIR_CODE.equals(userFile.getIsDir())) {
            List<UserFile> children = userFileRepository.findByPid(userFile.getId());
            for (UserFile child : children) {
                addDeletingFile(deletingFiles, child);
            }
        }
        deletingFiles.add(userFile);
    }

    public String saveFile(InputStream input) throws IOException {
        Path folder = Paths.get(appConfig.getFileRepository(), Strings.join(Arrays.stream(RandomUtil.randomString(appConfig.getFileDeepLevel()).split("")).collect(Collectors.toList()), File.separatorChar));
        if (Files.notExists(folder)) {
            Files.createDirectories(folder);
        }
        Path file = Paths.get(folder.toString(), UUID.randomUUID().toString().replaceAll("-", ""));
        Files.copy(input, file);
        return file.toString();
    }

    public PageInfo<UserFile> findPageList(Integer userId, String name, Integer pid, Integer pageNum, Integer pageSize) {
        name = SqlUtils.trimAndEscape(name);
        List<Sort.Order> orders = new ArrayList<>();
        orders.add(new Sort.Order(Sort.Direction.ASC, "filename"));
        orders.add(new Sort.Order(Sort.Direction.DESC, "id"));
        Pageable pageable = PageRequest.of(pageNum - 1, pageSize, Sort.by(orders));
        Page<UserFile> page;
        if (StrUtil.isNotBlank(name) && pid == null) {
            page = userFileRepository.findByUserIdAndFilenameLike(userId, "%" + name + "%", pageable);
        } else {
            page = userFileRepository.findByUserIdAndPid(userId, pid, pageable);
        }
        return new PageInfo<>(page.getContent(), page.getTotalPages(), page.getTotalElements());
    }

    public List<UserFile> findFloorsByPid(Integer pid) {
        List<UserFile> floors = new ArrayList<>();
        while (pid != null) {
            UserFile userFile = userFileRepository.findById(pid).orElse(null);
            if (userFile != null) {
                floors.add(userFile);
                pid = userFile.getPid();
            }
        }
        Collections.reverse(floors);
        return floors;
    }

    public UserFile findByShareId(String shareId) {
        return userFileRepository.findByShareId(shareId);
    }

    public UserFile findByPidAndFilename(Integer pid, String filename) {
        return userFileRepository.findByPidAndFilename(pid, filename);
    }

    public List<Integer> findAllChildDir(UserFile file) {
        List<Integer> list = new ArrayList<>();
        buildList(file, list);
        return list;
    }

    private void buildList(UserFile file, List<Integer> list) {
        if (file != null && PublicConstants.DIR_CODE.equals(file.getIsDir())) {
            list.add(file.getId());
            userFileRepository.findByPid(file.getId()).forEach(c -> buildList(c, list));
        }
    }

    public ShareFileDto getShareFile(Integer id, String shareId) {
        UserFile userFile = userFileRepository.findByShareId(shareId);
        AssertUtils.isTrue(userFile != null && Objects.equals(id, userFile.getId()), ErrorConstants.RESOURCE_NOT_FOUND_MSG);
        AssertUtils.isTrue(!PublicConstants.DIR_CODE.equals(userFile.getIsDir()) && StrUtil.isNotBlank(userFile.getPath()), "数据异常");
        Path file = Paths.get(userFile.getPath());
        AssertUtils.isTrue(Files.exists(file), ErrorConstants.FILE_IS_DELETED_MSG);
        return new ShareFileDto().setFilename(userFile.getFilename()).setPath(file);
    }

    public UserCapacityDto rootCount() {
        SecurityUser user = ContextUtils.getCurrentUser();
        List<UserFile> list = findRootList(user.getId());
        return new UserCapacityDto().setId(user.getId()).setRootCount(list.size());
    }

    public Boolean existFile(Integer id) {
        return userFileRepository.findById(id).filter(f -> StrUtil.isNotBlank(f.getPath()) && Files.exists(Paths.get(f.getPath()))).isPresent();
    }

    public String share(Integer id) {
        UserFile file = userFileRepository.findById(id).orElse(null);
        AssertUtils.notNull(file, "文件不存在");
        AssertUtils.isTrue(StrUtil.isNotBlank(file.getPath()) && Files.exists(Paths.get(file.getPath())), "文件已损坏");
        if (StrUtil.isBlank(file.getShareId())) {
            file.setShareId(UUID.randomUUID().toString().replaceAll("-", ""));
            userFileRepository.save(file);
        }
        return file.getShareId();
    }

    public FilePageDto findPage(String name, Integer pid, Integer pageNum, Integer pageSize) {
        SecurityUser user = ContextUtils.getCurrentUser();
        PageInfo<UserFile> page = findPageList(user.getId(), name, pid, pageNum, pageSize);
        List<UserFile> floors = findFloorsByPid(pid);
        return new FilePageDto().setParent(pid).setPage(page).setFloors(floors);
    }

    public Boolean rename(Integer id, String filename) {
        AssertUtils.isTrue(StrUtil.isNotBlank(filename), "文件名不能为空");
        UserFile file = userFileRepository.findById(id).orElseThrow(() -> new BizException("文件不存在"));
        file.setFilename(filename);
        file.setUpdateTime(new Date());
        userFileRepository.save(file);
        return true;
    }

    public DownloadFileDto getFile(Integer id) {
        UserFile userFile = userFileRepository.findById(id).orElse(null);
        AssertUtils.notNull(userFile, ErrorConstants.RESOURCE_NOT_FOUND_MSG);
        AssertUtils.isTrue(!PublicConstants.DIR_CODE.equals(userFile.getIsDir()), "暂不支持文件夹下载");
        AssertUtils.isTrue(StrUtil.isNotBlank(userFile.getPath()), "数据异常");
        Path file = Paths.get(userFile.getPath());
        AssertUtils.isTrue(Files.exists(file), ErrorConstants.FILE_IS_DELETED_MSG);
        return new DownloadFileDto().setFilename(userFile.getFilename()).setPath(file);
    }

    public void downloadAll(OutputStream outputStream) {
        ZipOutputStream out = null;
        try {
            SecurityUser user = ContextUtils.getCurrentUser();
            String tempPath = ContextUtils.getRequest().getServletContext().getRealPath("/");
            Path packageFile = packageFile(user.getId(), tempPath);
            AssertUtils.notNull(packageFile, ErrorConstants.RESOURCE_NOT_FOUND_MSG);
            out = new ZipOutputStream(outputStream);
            out.setMethod(ZipEntry.DEFLATED);
            out.setLevel(appConfig.getFileCompressLevel());
            // 压缩文件
            ZipUtils.compressFile(out, packageFile.toFile());
            // 删除临时文件
            cleanHistory(packageFile);
        } catch (Exception e) {
            throw new BizException("备份失败");
        } finally {
            IoUtil.close(out);
        }
    }

    public List<Map<String, Object>> getTree() {
        List<Map<String, Object>> list = new ArrayList<>();
        Map<String, Object> element = new HashMap<>();
        element.put("title", "全部文件");
        element.put("id", null);
        element.put("children", getFolderTreeData(null));
        list.add(element);
        return list;
    }

    private List<Map<String, Object>> getFolderTreeData(Integer pid) {
        List<Map<String, Object>> list = new ArrayList<>();
        List<UserFile> files = userFileRepository.findByPid(pid);
        for (UserFile f : files) {
            if (PublicConstants.DIR_CODE.equals(f.getIsDir())) {
                Map<String, Object> map = new HashMap<>();
                map.put("title", HtmlUtil.escape(f.getFilename()));
                map.put("id", f.getId());
                map.put("children", getFolderTreeData(f.getId()));
                list.add(map);
            }
        }
        return list;
    }

    public String viewAsText(Integer id) {
        UserFile file = userFileRepository.findById(id).orElse(null);
        AssertUtils.notNull(file, "文件不存在");
        String suffix = FileUtil.getSuffix(file.getFilename());
        AssertUtils.isTrue(StrUtil.isNotBlank(suffix) && Optional.ofNullable(appConfig.getFileSuffixes()).orElse("").contains(suffix), "文件类型不支持查看");
        StringBuilder sb = new StringBuilder();
        try (FileChannel channel = new RandomAccessFile(file.getPath(), "r").getChannel()) {
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            while (channel.read(buffer) != -1) {
                buffer.flip();
                sb.append(StandardCharsets.UTF_8.decode(buffer));
                buffer.clear();
            }
        } catch (IOException ignored) {
            throw new BizException("文件读取失败");
        }
        return sb.toString();
    }

    public void move(String ids, Integer pid) {
        for (String id : ids.split(PublicConstants.ID_DELIMITER)) {
            userFileRepository.findById(Integer.valueOf(id)).ifPresent(file -> {
                // 不能移动到自己或其子文件夹
                List<Integer> list = findAllChildDir(file);
                if (!list.contains(pid)) {
                    file.setPid(pid);
                    userFileRepository.save(file);
                }
            });
        }
    }

    public void newFolder(String filename, Integer pid) {
        UserFile file = userFileRepository.findByPidAndFilename(pid, filename);
        AssertUtils.isNull(file, "文件夹已存在");
        SecurityUser user = ContextUtils.getCurrentUser();
        Date now = new Date();
        UserFile file1 = new UserFile().setUserId(user.getId()).setPid(pid).setCreateTime(now).setUpdateTime(now).setFilename(filename).setIsDir(1).setSize(0L);
        userFileRepository.save(file1);
    }

    public void deleteMore(String ids) throws IOException {
        SecurityUser user = ContextUtils.getCurrentUser();
        String[] split = ids.split(PublicConstants.ID_DELIMITER);
        for (String s : split) {
            deleteById(Integer.valueOf(s), user.getId());
        }
    }

    public void upload(MultipartFile[] files, Integer pid) throws IOException {
        SecurityUser securityUser = ContextUtils.getCurrentUser();
        User user = userRepository.findById(securityUser.getId()).orElseThrow(() -> new BizException("用户不存在"));
        AssertUtils.isTrue(Optional.ofNullable(user.getCapacity()).orElse(0L) > 0, ErrorConstants.NO_SPACE_LEFT_MSG);
        long restSize = Optional.ofNullable(user.getCapacity()).orElse(0L) - Optional.ofNullable(user.getUsedSize()).orElse(0L);
        long totalSize = 0;
        for (MultipartFile file : files) {
            totalSize += file.getSize();
        }
        long freeSize = Arrays.stream(File.listRoots()).mapToLong(File::getFreeSpace).sum();
        Date now = new Date();
        AssertUtils.isTrue(restSize > totalSize && freeSize * PublicConstants.DISK_LIMIT_RATE > totalSize, ErrorConstants.NO_SPACE_LEFT_MSG);
        for (MultipartFile file : files) {
            String path = saveFile(file.getInputStream());
            String filename = Objects.requireNonNull(file.getOriginalFilename()).replaceAll(" ", "+");
            UserFile userFile = new UserFile().setUserId(user.getId()).setPid(pid).setCreateTime(now).setUpdateTime(now).setFilename(getNoRepeatFilename(pid, filename)).setPath(path).setSize(file.getSize());
            userFileRepository.save(userFile);
            long newSize = Optional.ofNullable(user.getUsedSize()).orElse(0L) + file.getSize();
            long capacity = Optional.ofNullable(user.getCapacity()).orElse(0L);
            user.setUsedSize(Math.min(newSize, capacity));// 容量误差修正
            userRepository.save(user);
        }
    }

    private String getNoRepeatFilename(Integer pid, String filename) {
        String prefix = FileUtil.getPrefix(filename);
        String suffix = FileUtil.getSuffix(filename);
        String name = filename;
        int index = 0;
        while (userFileRepository.findByPidAndFilename(pid, name) != null) {
            name = prefix + "_" + ++index + (StrUtil.isNotBlank(suffix) ? "." + suffix : "");
        }
        return name;
    }
}

